"""
Configuration Manager for GSM Extension System

This module provides JSON Schema-based configuration validation and management
for GSM extensions. It handles configuration loading, validation, inheritance,
and migration.
"""

import json
import jsonschema
from typing import Dict, List, Optional, Any, Union
from pathlib import Path
from dataclasses import dataclass, field
import logging
import copy
from datetime import datetime


@dataclass
class ConfigurationError(Exception):
    """Configuration-related error."""
    
    config_path: Optional[Path] = None
    schema_path: Optional[Path] = None
    validation_errors: List[str] = field(default_factory=list)
    
    def __str__(self) -> str:
        """String representation of error."""
        msg = "Configuration error"
        if self.config_path:
            msg += f" in {self.config_path}"
        if self.validation_errors:
            msg += f": {'; '.join(self.validation_errors)}"
        return msg


@dataclass
class ConfigValidationResult:
    """Result of configuration validation."""
    
    valid: bool
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    schema_path: Optional[Path] = None
    config_path: Optional[Path] = None
    
    def add_error(self, message: str) -> None:
        """Add validation error."""
        self.errors.append(message)
        self.valid = False
    
    def add_warning(self, message: str) -> None:
        """Add validation warning."""
        self.warnings.append(message)


class ConfigSchema:
    """JSON Schema wrapper for configuration validation."""
    
    def __init__(self, schema_data: Dict[str, Any], schema_path: Optional[Path] = None):
        """
        Initialize configuration schema.
        
        Args:
            schema_data: JSON Schema dictionary
            schema_path: Optional path to schema file
        """
        self.schema_data = schema_data
        self.schema_path = schema_path
        
        # Validate schema itself
        try:
            jsonschema.validators.Draft7Validator.check_schema(schema_data)
            self._validator = jsonschema.validators.Draft7Validator(schema_data)
        except jsonschema.SchemaError as e:
            raise ConfigurationError(
                schema_path=schema_path,
                validation_errors=[f"Invalid schema: {str(e)}"]
            )
    
    def validate(self, config_data: Dict[str, Any]) -> ConfigValidationResult:
        """
        Validate configuration against schema.
        
        Args:
            config_data: Configuration data to validate
            
        Returns:
            ConfigValidationResult: Validation result
        """
        result = ConfigValidationResult(valid=True, schema_path=self.schema_path)
        
        try:
            # Perform validation
            errors = sorted(self._validator.iter_errors(config_data), key=lambda e: e.path)
            
            for error in errors:
                # Format error message with path
                path = ".".join(str(p) for p in error.absolute_path) if error.absolute_path else "root"
                message = f"At '{path}': {error.message}"
                result.add_error(message)
            
        except Exception as e:
            result.add_error(f"Validation error: {str(e)}")
        
        return result
    
    def get_default_config(self) -> Dict[str, Any]:
        """
        Generate default configuration from schema.
        
        Returns:
            Dict[str, Any]: Default configuration based on schema defaults
        """
        def extract_defaults(schema_part: Any) -> Any:
            if isinstance(schema_part, dict):
                result = {}
                
                # Handle object properties
                if "properties" in schema_part:
                    for prop, prop_schema in schema_part["properties"].items():
                        if "default" in prop_schema:
                            result[prop] = prop_schema["default"]
                        elif prop_schema.get("type") == "object":
                            default_obj = extract_defaults(prop_schema)
                            if default_obj:
                                result[prop] = default_obj
                        elif prop_schema.get("type") == "array" and "items" in prop_schema:
                            if "default" in prop_schema["items"]:
                                result[prop] = []
                
                return result
            
            return None
        
        return extract_defaults(self.schema_data) or {}
    
    def get_required_fields(self) -> List[str]:
        """Get list of required fields from schema."""
        return self.schema_data.get("required", [])
    
    def get_field_description(self, field_path: str) -> Optional[str]:
        """Get description for a specific field."""
        parts = field_path.split(".")
        current = self.schema_data
        
        try:
            # Navigate to field in schema
            for part in parts:
                if "properties" in current and part in current["properties"]:
                    current = current["properties"][part]
                else:
                    return None
            
            return current.get("description")
        
        except Exception:
            return None


class ExtensionConfigManager:
    """
    Configuration manager for GSM extensions.
    
    Provides JSON Schema-based configuration validation, inheritance,
    and management functionality for extensions.
    """
    
    def __init__(self, logger: Optional[logging.Logger] = None):
        """Initialize configuration manager."""
        self._logger = logger or logging.getLogger(__name__)
        
        # Configuration storage
        self._extension_schemas: Dict[str, ConfigSchema] = {}
        self._extension_configs: Dict[str, Dict[str, Any]] = {}
        
        # Configuration hierarchy (higher priority first)
        self._config_search_paths = [
            Path.cwd() / "gsm-extensions.yaml",
            Path.cwd() / "gsm-extensions.json", 
            Path.home() / ".gsm" / "config" / "extensions.yaml",
            Path.home() / ".gsm" / "config" / "extensions.json",
            Path("/etc/gsm/extensions.yaml"),
            Path("/etc/gsm/extensions.json"),
        ]
        
        self._logger.debug("Extension configuration manager initialized")
    
    def register_extension_schema(self, extension_name: str, 
                                 schema_data: Dict[str, Any],
                                 schema_path: Optional[Path] = None) -> bool:
        """
        Register schema for an extension.
        
        Args:
            extension_name: Name of extension
            schema_data: JSON Schema dictionary
            schema_path: Optional path to schema file
            
        Returns:
            bool: True if registration successful
        """
        try:
            schema = ConfigSchema(schema_data, schema_path)
            self._extension_schemas[extension_name] = schema
            
            self._logger.debug(f"Registered schema for extension: {extension_name}")
            return True
            
        except ConfigurationError as e:
            self._logger.error(f"Failed to register schema for {extension_name}: {str(e)}")
            return False
    
    def load_extension_config(self, extension_name: str, 
                             config_path: Optional[Path] = None) -> Dict[str, Any]:
        """
        Load configuration for an extension.
        
        Args:
            extension_name: Name of extension
            config_path: Optional specific configuration file path
            
        Returns:
            Dict[str, Any]: Extension configuration
        """
        try:
            # Start with default configuration from schema
            config = {}
            if extension_name in self._extension_schemas:
                config = self._extension_schemas[extension_name].get_default_config()
            
            # Load configurations from hierarchy
            hierarchy_config = self._load_config_hierarchy(extension_name)
            if hierarchy_config:
                config = self._merge_configs(config, hierarchy_config)
            
            # Load specific config file if provided
            if config_path and config_path.exists():
                specific_config = self._load_config_file(config_path)
                if specific_config and extension_name in specific_config.get("extension_configs", {}):
                    extension_specific = specific_config["extension_configs"][extension_name]
                    config = self._merge_configs(config, extension_specific)
            
            # Store loaded config
            self._extension_configs[extension_name] = config
            
            self._logger.debug(f"Loaded configuration for extension: {extension_name}")
            return config
            
        except Exception as e:
            self._logger.error(f"Error loading configuration for {extension_name}: {str(e)}")
            return {}
    
    def validate_extension_config(self, extension_name: str, 
                                 config_data: Optional[Dict[str, Any]] = None) -> ConfigValidationResult:
        """
        Validate extension configuration against schema.
        
        Args:
            extension_name: Name of extension
            config_data: Optional configuration data (uses loaded config if not provided)
            
        Returns:
            ConfigValidationResult: Validation result
        """
        result = ConfigValidationResult(valid=True)
        
        try:
            # Get schema for extension
            if extension_name not in self._extension_schemas:
                result.add_error(f"No schema registered for extension: {extension_name}")
                return result
            
            schema = self._extension_schemas[extension_name]
            
            # Get configuration data
            if config_data is None:
                if extension_name not in self._extension_configs:
                    self.load_extension_config(extension_name)
                config_data = self._extension_configs.get(extension_name, {})
            
            # Validate configuration
            return schema.validate(config_data)
            
        except Exception as e:
            result.add_error(f"Validation error: {str(e)}")
            return result
    
    def get_extension_config(self, extension_name: str) -> Dict[str, Any]:
        """
        Get loaded configuration for extension.
        
        Args:
            extension_name: Name of extension
            
        Returns:
            Dict[str, Any]: Extension configuration
        """
        if extension_name not in self._extension_configs:
            return self.load_extension_config(extension_name)
        return self._extension_configs[extension_name].copy()
    
    def update_extension_config(self, extension_name: str, 
                               config_updates: Dict[str, Any],
                               validate: bool = True) -> bool:
        """
        Update extension configuration.
        
        Args:
            extension_name: Name of extension
            config_updates: Configuration updates
            validate: Whether to validate updated configuration
            
        Returns:
            bool: True if update successful
        """
        try:
            # Get current config
            current_config = self.get_extension_config(extension_name)
            
            # Merge updates
            updated_config = self._merge_configs(current_config, config_updates)
            
            # Validate if requested
            if validate:
                result = self.validate_extension_config(extension_name, updated_config)
                if not result.valid:
                    self._logger.error(f"Configuration validation failed for {extension_name}: {result.errors}")
                    return False
            
            # Update stored config
            self._extension_configs[extension_name] = updated_config
            
            self._logger.debug(f"Updated configuration for extension: {extension_name}")
            return True
            
        except Exception as e:
            self._logger.error(f"Error updating configuration for {extension_name}: {str(e)}")
            return False
    
    def save_extension_config(self, extension_name: str, 
                             config_path: Optional[Path] = None) -> bool:
        """
        Save extension configuration to file.
        
        Args:
            extension_name: Name of extension
            config_path: Optional path to save configuration
            
        Returns:
            bool: True if save successful
        """
        try:
            if extension_name not in self._extension_configs:
                self._logger.error(f"No configuration loaded for extension: {extension_name}")
                return False
            
            config = self._extension_configs[extension_name]
            
            # Use default path if not provided
            if not config_path:
                config_path = Path.home() / ".gsm" / "config" / f"{extension_name}.json"
            
            # Ensure directory exists
            config_path.parent.mkdir(parents=True, exist_ok=True)
            
            # Save configuration
            with open(config_path, 'w') as f:
                json.dump(config, f, indent=2)
            
            self._logger.debug(f"Saved configuration for {extension_name} to {config_path}")
            return True
            
        except Exception as e:
            self._logger.error(f"Error saving configuration for {extension_name}: {str(e)}")
            return False
    
    def get_schema_info(self, extension_name: str) -> Optional[Dict[str, Any]]:
        """
        Get schema information for extension.
        
        Args:
            extension_name: Name of extension
            
        Returns:
            Optional[Dict[str, Any]]: Schema information
        """
        if extension_name not in self._extension_schemas:
            return None
        
        schema = self._extension_schemas[extension_name]
        return {
            "schema_path": str(schema.schema_path) if schema.schema_path else None,
            "required_fields": schema.get_required_fields(),
            "default_config": schema.get_default_config(),
            "title": schema.schema_data.get("title"),
            "description": schema.schema_data.get("description"),
        }
    
    def list_extension_configs(self) -> List[str]:
        """List names of extensions with loaded configurations."""
        return list(self._extension_configs.keys())
    
    def clear_extension_config(self, extension_name: str) -> None:
        """Clear loaded configuration for extension."""
        if extension_name in self._extension_configs:
            del self._extension_configs[extension_name]
            self._logger.debug(f"Cleared configuration for extension: {extension_name}")
    
    def _load_config_hierarchy(self, extension_name: str) -> Optional[Dict[str, Any]]:
        """Load configuration from hierarchy of config files."""
        merged_config = {}
        
        # Load from search paths (reverse order for proper priority)
        for config_path in reversed(self._config_search_paths):
            if config_path.exists():
                try:
                    file_config = self._load_config_file(config_path)
                    if file_config and "extension_configs" in file_config:
                        extension_configs = file_config["extension_configs"]
                        if extension_name in extension_configs:
                            merged_config = self._merge_configs(
                                merged_config, 
                                extension_configs[extension_name]
                            )
                
                except Exception as e:
                    self._logger.warning(f"Error loading config from {config_path}: {str(e)}")
        
        return merged_config if merged_config else None
    
    def _load_config_file(self, config_path: Path) -> Optional[Dict[str, Any]]:
        """Load configuration from a single file."""
        try:
            with open(config_path, 'r') as f:
                if config_path.suffix.lower() in ['.yaml', '.yml']:
                    try:
                        import yaml
                        return yaml.safe_load(f)
                    except ImportError:
                        self._logger.error(f"YAML support not available, cannot load {config_path}")
                        return None
                else:
                    return json.load(f)
        
        except Exception as e:
            self._logger.error(f"Error loading config file {config_path}: {str(e)}")
            return None
    
    def _merge_configs(self, base_config: Dict[str, Any], 
                      override_config: Dict[str, Any]) -> Dict[str, Any]:
        """
        Merge two configuration dictionaries.
        
        Args:
            base_config: Base configuration
            override_config: Configuration to overlay on base
            
        Returns:
            Dict[str, Any]: Merged configuration
        """
        result = copy.deepcopy(base_config)
        
        def merge_dict(base: Dict[str, Any], override: Dict[str, Any]) -> None:
            for key, value in override.items():
                if key in base and isinstance(base[key], dict) and isinstance(value, dict):
                    merge_dict(base[key], value)
                else:
                    base[key] = copy.deepcopy(value)
        
        merge_dict(result, override_config)
        return result